package Clases;

import java.net.*;
import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class Cliente {

    private List<Direccion> servidoresConocidos;
    private Direccion direccion;
    private int level, split;   //Variables que usará el cliente para mantener información para el cálculo 
    //del balde al que debe enviar la consulta

    public Cliente(int id, String ip, int puertoRecibir) {
        servidoresConocidos = LectorArchivos.ObtenerDireccionesServidores("src/ListaServidoresDelCliente.csv");
        calcularLevelYSplit();      // En base a los servers conocidos se calcula el level y split
        direccion = new Direccion(id, ip, puertoRecibir, level, split);
    }

    public void run() {
        try {
            //Lee el comando en pantalla del cliente
            BufferedReader bufferConsola = new BufferedReader(new InputStreamReader(System.in));

            //Cliente abre socket para escuchar respuestas
            ServerSocket listenerCliente = new ServerSocket();
            listenerCliente.bind(new InetSocketAddress(direccion.IP(), direccion.PuertoRecibir()));

            System.out.println("Cliente " + direccion.ID() + " Escuchando en: "
                    + direccion.IP() + ":" + direccion.PuertoRecibir());

            String line = "";
            String datosCliente = "";

            while (line != null && !line.equalsIgnoreCase("quit")) {
                datosCliente = "#" + direccion.ID() + ";"
                        + direccion.IP() + ";"
                        + direccion.PuertoRecibir() + ";"
                        + level + ";"
                        + split;

                System.out.print("> ");

                line = bufferConsola.readLine();
                Socket serverAConsultar = identificarServer(line);  //Se calcula el servidor al que se debe consultar
                if (serverAConsultar != null) {

                    PrintWriter escritorSockets = null;
                    escritorSockets = escribirSocket(serverAConsultar, escritorSockets, line);             //y se envía la consulta
                    //Luego envía la IP y puerto en la que espera la respuesta
                    escritorSockets = escribirSocket(serverAConsultar, escritorSockets, datosCliente);
                    serverAConsultar.close();
                    escritorSockets.close();

                    System.out.println("Esperando respuesta desde servidor...");
                    //Acá recibe las respuestas de los servidores hay que ver como hacer con el scan para que espere la
                    //respuesta de todos los servers, los conozca o no
                    int timeOutConexionServer = 0;
                    Boolean seRecibioRespuestaDeQuery = false;
                    BufferedReader reader=null;
                    if (!ProcesaConsultas.obtenerOperacion(line).equals(ProcesaConsultas.Operaciones.SCAN)) {
                        try {
                            timeOutConexionServer = 2000; //VAN 2000 ACA
                            //Seteamos un timeout para la respuesta que envíe el servidor
                            listenerCliente.setSoTimeout(timeOutConexionServer);

                            Socket respuestaServer = listenerCliente.accept();
                            reader = new BufferedReader(new InputStreamReader(respuestaServer.getInputStream()));
                            String respuestaDeServidor = reader.readLine();
                            System.out.printf("Respuesta: %s \n", respuestaDeServidor);
                            seRecibioRespuestaDeQuery = true;

                            respuestaServer = listenerCliente.accept();
                            reader = new BufferedReader(new InputStreamReader(respuestaServer.getInputStream()));
                            respuestaDeServidor = reader.readLine();
                            System.out.println("Ajuste: " + respuestaDeServidor);
                            InterpretarMensajeDeAjuste(respuestaDeServidor);
                        } catch (SocketTimeoutException timeout) {
                                //se podria limpiar el buffer por el corrimiento de msjs con el timeout
                            if (seRecibioRespuestaDeQuery) {
                                System.out.println("No se ha recibido mensaje de ajuste");
                            } else {
                                System.out.println("La respuesta ha excedido el timeout de " + timeOutConexionServer + "ms");
                            }
                        }
                    } else {

                        //SCAN como lo hacemos??
                        int timeOutScan = 2000;
                        listenerCliente.setSoTimeout(timeOutScan);
                        System.out.println("Esperando respuesta de los Scan");
                        String respuestaScanBaldes = "";
                        while (true) {
                            try {
                                Socket respuestaServer = listenerCliente.accept();
                                reader = new BufferedReader(new InputStreamReader(respuestaServer.getInputStream()));
                                respuestaScanBaldes += reader.readLine() + " ";
                            } catch (SocketTimeoutException timeout) {
                                //se podria limpiar el buffer por el corrimiento de msjs con el timeout
                                break;
                            }
                            //hay que ver como meterle el ajuste aca
//                            respuestaServer = listenerCliente.accept();
//                            reader = new BufferedReader(new InputStreamReader(respuestaServer.getInputStream()));
//                            respuestaDeServidor = reader.readLine();
//                            System.out.println("Ajuste: " + respuestaDeServidor);
//                            InterpretarMensajeDeAjuste(respuestaDeServidor);
                        }
                        System.out.println("Respuesta del Scan: " + respuestaScanBaldes);
                    }

                } else {
                    System.err.println("Error: No se pudo establecer conexión con servidor");
                }
            }
        } catch (Exception ex) {
            System.err.println("Error: " + ex.getMessage());
        }
    }

    /**
     * A partir de la consulta ingresada se determina a qué servidor debe enviarse la consulta
     * @param line
     * @return El socket conectado o nulo si hay error
     */
    private Socket identificarServer(String line) {
        Direccion serverConsulta = getDirServer(line);
        Socket socket = new Socket();
        try {
            socket.connect(new InetSocketAddress(serverConsulta.IP(), serverConsulta.PuertoRecibir()));
        } catch (Exception ex) {
            System.err.println("Error: " + ex.getMessage());
            return null;
        }
        return socket;
    }

    private Direccion getDirServer(String line) {
        int idServer = 0;
        ProcesaConsultas.Operaciones oper = Parser.getOperacion(line);
        switch (oper) {
            case SELECT:       /// SELECT_3
            case UPDATE:       /// UPDATE_3;VALOR
            case INSERT:       /// INSERT_3;VALOR
            case DELETE:       /// DELETE_3;VALOR
                idServer = calcularIDBalde(Parser.getRID(line));    //Obtengo el id del server al que le debo enviar la consulta
                break;
            default:
                break;
        }
        //Se envía la consulta al server en la posición idServer. En caso de que la consulta
        //no sea una de las cuatro en el case se envía al que está en la posición 0
        return this.servidoresConocidos.get(idServer);
    }

    private PrintWriter escribirSocket(Socket serverAConsultar, PrintWriter writer, String line) {
        try {
            writer = new PrintWriter(serverAConsultar.getOutputStream());  //Escribirá los comandos en el socket
            writer.write(line + "\n");
            writer.flush();
            return writer;
        } catch (Exception ex) {
            System.err.println("Error: " + ex.getMessage());
            return null;
        }
    }

    private int calcularHash(int rID) {
        //Operación implementada: h = RID % 2**(level + 1)
        int hash = rID % (int) Math.pow(2, this.level + 1);
        return hash;
    }

    private int calcularIDBalde(int rID) {
        int idBalde = rID % (int) Math.pow(2, this.level);
        if (idBalde < this.split) {
            idBalde = calcularHash(rID);
        }
        return idBalde;
    }

    /***
     * el formato del msj de ajuste de baldes será:
     * UPD_level_split_idBalde1,IPBalde1,PuertoBalde1;idBalde,IPBalde2,PuertoBalde2...
     *
     * Revisar si quedó bien
     * @param mensaje
     */
    private void InterpretarMensajeDeAjuste(String mensajeAjuste) {
        if (mensajeAjuste != null) {
            String[] mensajeParseado = mensajeAjuste.split("_");
            if (mensajeParseado.length == 4) {
                int i = 0;
                if (mensajeParseado[i].equalsIgnoreCase("UPD")) {
                    // Level
                    i++;
                    this.level = Integer.parseInt(mensajeParseado[i]);
                    direccion.Level(this.level);

                    // Split
                    i++;
                    this.split = Integer.parseInt(mensajeParseado[i]);
                    direccion.Split(this.split);
                    // Lista actualizada de direcciones
                    i++;
                    String[] baldesNuevos = mensajeParseado[i].split(";");
                    Boolean servidorYaConocido = false;
                    for (String baldeNuevo : baldesNuevos) {
                        String[] datosDeBalde = baldeNuevo.split(",");
                        int idNuevoBalde = Integer.parseInt(datosDeBalde[0]);
                        String ipNuevoBalde = datosDeBalde[1];
                        int puertoNuevoBalde = Integer.parseInt(datosDeBalde[2]);
                        // Reviso la lista actual de direcciones para agregarla en caso de que no esté
                        for (Direccion dir : servidoresConocidos) {
                            if (idNuevoBalde == dir.ID()) {
                                servidorYaConocido = true;
                                break;
                            }
                        }
                        if (!servidorYaConocido) {
                            servidoresConocidos.add(new Direccion(idNuevoBalde, ipNuevoBalde, puertoNuevoBalde));
                        }
                        servidorYaConocido = false;
                    }
                }
            }
        }
    }

    /***
     * 
     * Este método se encarga de calcular el Level y el Split a partir de la
     * cantidad de servidores conocidos que se obtienen de archivo
     */
    private void calcularLevelYSplit() {
        // 2^level+split = numeroServers
        int nroServers = this.servidoresConocidos.size();
        double log2Servers = (Math.log(nroServers) / Math.log(2));
        this.level = (int) Math.floor(log2Servers);
        this.split = nroServers - (int) Math.pow(2, this.level);
    }
}
